#include "bt.h"
#include "bencode.h"
#include "dht.h"
#include "epoll_loop.h"
#include "file.h"
#include "fstream.h"
#include <cstdint>
#include <cstring>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <istream>
#include <netinet/in.h>
#include <openssl/sha.h>
#include <sstream>
#include <string>
#include <sys/socket.h>

using namespace std::chrono_literals;

namespace cobt {
GetMetaFile::GetMetaFile(const SocketAddress &addr,
                         const std::string &info_hash, const std::string &id)
    : m_addr(addr), m_info_hash(info_hash), m_id(id), m_fs(20s) {
  int fd;
  if (addr.get_family() == AF_INET6) {
    fd = socket(AF_INET6, SOCK_STREAM, 0);
  } else {
    fd = socket(AF_INET, SOCK_STREAM, 0);
  }
  int opt = 0;
  setsockopt(fd, IPPROTO_IPV6, IPV6_V6ONLY, &opt, sizeof(opt));
  int flags = fcntl(fd, F_GETFL, 0);
  fcntl(fd, F_SETFL, flags | O_NONBLOCK);
  m_fs.set_handle(FileHandle(fd));
}
Task<bool> GetMetaFile::recv_msg(std::string &buf) {
  auto len_bytes = co_await m_fs.getn(4);
  if (len_bytes.size() != 4) {
    co_return false;
  }
  uint32_t len = ntohl(*(uint32_t *)(len_bytes.data()));
  buf = co_await m_fs.getn(len);
  if (buf.size() != len) {
    co_return false;
  }
  co_return true;
}

Task<bool> GetMetaFile::handshake() {
  if (co_await connect_async(m_fs.get_fd(), m_addr, 5s) != 0) {
    co_return false;
  }
  char handshake[68] = {19,   'B',  'i',  't',  'T',  'o',  'r',  'r', 'e', 'n',
                        't',  ' ',  'p',  'r',  'o',  't',  'o',  'c', 'o', 'l',
                        0x00, 0x00, 0x00, 0x00, 0x00, 0x10, 0x00, 0x04};
  memcpy(handshake + 28, m_info_hash.data(), 20);
  memcpy(handshake + 48, m_id.data(), 20);
  if (!co_await m_fs.puts(std::string(handshake, 68))) {
    co_return false;
  }
  if (!co_await m_fs.flush()) {
    co_return false;
  }
  std::string recv_buf = co_await m_fs.getn(68);
  if (recv_buf.size() != 68) {
    co_return false;
  }
  if (memcmp(recv_buf.data(), handshake, 20) != 0) {
    co_return false;
  }
  if (!(recv_buf[25] & 0x10)) {
    co_return false;
  }
  co_return true;
}
bool GetMetaFile::ext_handshake(std::istream &ss) {
  auto bdata = BEncodeData::parse(ss);
  if (!bdata) {
    return false;
  }
  auto m = bdata.value()["m"];
  if (!m) {
    return false;
  }
  auto ut_metadata = (*m)["ut_metadata"];
  if (!ut_metadata) {
    return false;
  }
  if (auto um = ut_metadata->as<int64_t>(); !um) {
    return false;
  } else {
    m_ut_metadata = *um;
  }
  auto metadata_size = bdata.value()["metadata_size"];
  if (!metadata_size) {
    return false;
  }
  if (auto len = metadata_size->as<int64_t>(); !len) {
    return false;
  } else {
    m_metadata_size = *len;
  }
  return true;
}

Task<bool> GetMetaFile::send_msg(const std::string &msg, char msg_id) {
  auto len = htonl((msg.size() + 2));
  std::string req;
  req.reserve(4 + len);
  req.append((char *)&len, 4);
  req.append({20, msg_id});
  req.append(msg);
  if (!co_await m_fs.puts(req)) {
    co_return false;
  }
  if (!co_await m_fs.flush()) {
    co_return false;
  }
  co_return true;
}

Task<std::string> GetMetaFile::recv_metainfo(const DHTDigger &digger) {
  int64_t pieces = 0;
  std::string recv_buf;
  while (true) {
    if (!co_await recv_msg(recv_buf)) {
      co_return "";
    }
    // if (recv_buf[0] == 15) {
    //   co_return false;
    // }
    if (recv_buf[0] != 20) {
      continue;
    }
    if (recv_buf.size() < 2) {
      co_return "";
    }
    if (recv_buf[1] == 0) {
      std::cout << "ext handshake" << std::endl;
      std::stringstream ss(recv_buf.substr(2));
      if (!ext_handshake(ss)) {
        co_return "";
      }
      break;
    }
    if (recv_buf[1] != m_ut_metadata) {
      std::cout << "wrong ext msg type " << (int)recv_buf[1] << std::endl;
      continue;
    }
  }

  BEncodeData ext_msg{BMap{{"m", BMap{{"ut_metadata", 1}}}}};
  if (!co_await send_msg(ext_msg.dump(), 0)) {
    co_return "";
  }
  std::string meta_info;
  meta_info.reserve(m_metadata_size);
  while (m_metadata_size > 0) {
    BEncodeData msg{BMap{{"msg_type", 0}, {"piece", pieces}}};
    if (!co_await send_msg(msg.dump(), m_ut_metadata)) {
      co_return "";
    }
    while (true) {
      if (!co_await recv_msg(recv_buf)) {
        co_return "";
      }
      if (recv_buf[0] != 20) {
        continue;
      }
      if (recv_buf.size() < 2) {
        co_return "";
      }
      if (digger.info_hash_status(m_info_hash)) {
        co_return "0";
      }
      break;
    }

    std::stringstream ss(recv_buf.substr(2));

    auto bdata = BEncodeData::parse(ss);
    if (!bdata) {
      std::cout << "not bencode" << std::endl;
      co_return "";
    }
    auto pmsg_type = (*bdata)["msg_type"];
    if (!pmsg_type) {
      std::cout << "no msg_type" << std::endl;
      co_return "";
    }
    auto msg_type = pmsg_type->as<int64_t>();
    if (!msg_type) {
      std::cout << "no msg_type" << std::endl;
      co_return "";
    }
    if (*msg_type != 1) {
      continue;
    }
    auto ppiece = (*bdata)["piece"];
    if (!ppiece) {
      std::cout << "no piece" << std::endl;
      co_return "";
    }
    auto piece = ppiece->as<int64_t>();
    if (!piece) {
      std::cout << "no piece" << std::endl;
      co_return "";
    }
    if (*piece != pieces) {
      std::cout << "wrong piece " << *piece << "!=" << pieces << std::endl;
      co_return "";
    }
    std::string meta_buf = ss.str().substr(ss.tellg());
    std::cout << "piece " << pieces << ' ' << meta_buf.size() << std::endl;
    meta_info.append(std::move(meta_buf));

    pieces++;
    m_metadata_size -= 16384;
  }
  std::string recv_info_hash(20, 0);
  SHA1((unsigned char *)meta_info.data(), meta_info.size(),
       (unsigned char *)recv_info_hash.data());
  if (m_info_hash != recv_info_hash) {
    std::cout << "wrong info hash" << std::endl;
    co_return "";
  }
  auto bmeta = BEncodeData::parse(meta_info);
  if (!bmeta) {
    co_return "";
  }
  std::stringstream info_hash_ss;
  for (auto ch : m_info_hash) {
    info_hash_ss << std::hex << std::setw(2) << std::setfill('0')
                 << (unsigned int)(uint8_t)ch;
  }
  auto name = (*bmeta)["name"];
  std::string name_str = info_hash_ss.str();
  if (name) {
    name_str = std::move(*name->as<std::string>());
  }
  std::string torrent_file =
      "d8:announce45:wss://tracker.openwebtorrent.com:443/announce4:info" +
      meta_info + 'e';

  std::ofstream ofs(name_str + ".torrent",
                    std::ios::out | std::ios::binary | std::ios::trunc);
  ofs << torrent_file;
  ofs.flush();
  std::cout << "write to " << name_str + ".torrent" << std::endl;
  co_return name_str;
  // co_await m_fs.getn(4);
}
Task<std::string> GetMetaFile::get_meta_file(const DHTDigger &digger) {
  if (!co_await handshake()) {
    co_return "";
  }

  co_return co_await recv_metainfo(digger);
}

} // namespace cobt